package net.sf.ivyide.idea;

import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.module.ModifiableModuleModel;
import com.intellij.openapi.progress.ProgressIndicator;
import com.intellij.openapi.roots.*;
import com.intellij.openapi.roots.libraries.Library;
import com.intellij.openapi.roots.libraries.LibraryTable;
import com.intellij.openapi.vfs.LocalFileSystem;
import com.intellij.openapi.vfs.VirtualFile;
import com.intellij.openapi.vfs.VirtualFileManager;
import fr.jayasoft.ivy.ModuleId;
import fr.jayasoft.ivy.ModuleRevisionId;
import net.sf.ivyide.*;

import java.io.File;
import java.util.*;

public class UpdateModule
        implements ProgressAwareRunnable {

    private static final Logger LOGGER = Logger.getInstance(UpdateModule.class.getName());

    private ModuleManager m_moduleManager;
    private LibraryTable m_projectLibraryTable;
    private Map<String, Library> m_usedProjectLibraries;
    private ModifiableModuleModel m_modulesModel;
    private ModifiableRootModel m_rootModel;
    private ResolvedModule m_resolvedModule;
    private Set<Library.ModifiableModel> m_libraryModelsToCommit;

    public UpdateModule(ModuleManager settings, ResolvedModule resolvedModule, ModifiableRootModel rootModel, LibraryTable projectLibraryTable, Map<String, Library> usedProjectLibraries, ModifiableModuleModel modulesModel, Set<Library.ModifiableModel> libraryModelsToCommit) {
        m_rootModel = rootModel;
        m_resolvedModule = resolvedModule;
        m_moduleManager = settings;
        m_projectLibraryTable = projectLibraryTable;
        m_usedProjectLibraries = usedProjectLibraries;
        m_libraryModelsToCommit = libraryModelsToCommit;
        m_modulesModel = modulesModel;
    }

    private Library synchronizeLib(final Map<String, Library> cachedLibraries, final LibraryTable libraryTable, final ModuleRevisionId libraryId, ModuleManager moduleManager, ResolvedModule module, ProgressIndicator progressIndicator, final Set<Library.ModifiableModel> libModelsToCommit) {
        String name = (new StringBuilder()).append(libraryId.getName()).append("-").append(libraryId.getRevision()).toString();
        Library cachedLibrary = cachedLibraries.get(name);
        if (cachedLibrary == null) { //no yet synced (same artifact can be synced by different modules)
            LibraryDescriptor descriptor = moduleManager.getLibraryDescriptor(module, libraryId);
            Library library = synchronizeLib(libraryTable, name, descriptor, progressIndicator, libModelsToCommit);
            cachedLibraries.put(name, library);
            return library;
        } else {
            return cachedLibrary;
        }

    }

    private Library synchronizeLib(final LibraryTable libraryTable, final String name, final LibraryDescriptor descriptor, ProgressIndicator progressIndicator, final Set<Library.ModifiableModel> libModelsToCommit) {
        progressIndicator.setText2("Synchronizing library " + name);

        return ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareCallable<Library>() {
            public Library call(ProgressIndicator progressIndicator) throws Exception {
                boolean modified = false;
                Library libraryByName = libraryTable.getLibraryByName(name);
                if (libraryByName == null) {
                    LOGGER.debug("New library " + name);
                    modified = true;
                    libraryByName = libraryTable.createLibrary(name);
                }
                Library.ModifiableModel libModifiableModel = libraryByName.getModifiableModel();
                VirtualFile[] classFiles = UpdateModule.this.createVirtualFiles(descriptor.getClasses(), progressIndicator);
                VirtualFile[] sourceFiles = UpdateModule.this.createVirtualFiles(descriptor.getSources(), progressIndicator);
                modified = modified | UpdateModule.syncRoots(classFiles, libModifiableModel, OrderRootType.CLASSES);
                modified = modified | UpdateModule.syncRoots(sourceFiles, libModifiableModel, OrderRootType.SOURCES);
                LOGGER.debug("Library modified " + name + ": " + modified);
                if (modified) {
                    progressIndicator.setText2("Updating library " + name);

                    String[] urls = libModifiableModel.getUrls(OrderRootType.CLASSES);
                    for (String url : urls) {
                        LOGGER.debug("CLASSES: " + url);
                    }
                    urls = libModifiableModel.getUrls(OrderRootType.SOURCES);
                    for (String url : urls) {
                        LOGGER.debug("SOURCES: " + url);
                    }
                    //libModelsToCommit.add(libModifiableModel);
                    libModifiableModel.commit();
                }
                return libraryByName;
            }
        }, progressIndicator);

    }

    private static boolean syncRoots(VirtualFile[] classes, Library.ModifiableModel libModifiableModel, OrderRootType rootType) {
        boolean modified = false;
        List<String> oldUrls = new LinkedList<String>(Arrays.asList(libModifiableModel.getUrls(rootType)));
        for (VirtualFile virtualFile : classes) {
            boolean alreadyAvailable = false;
            for (String oldUrl : oldUrls) {
                if (oldUrl.equals(virtualFile.getUrl())) {
                    alreadyAvailable = true;
                    oldUrls.remove(oldUrl);
                    break;
                }
            }
            if (!alreadyAvailable) {
                modified = true;
                libModifiableModel.addRoot(virtualFile, rootType);
            }
        }
        //Remove old stuff that is not available in the new stuff
        for (String oldUrl : oldUrls) {
            modified = true;
            UpdateModule.LOGGER.info("Removing " + oldUrl + " from " + libModifiableModel.getName());
            libModifiableModel.removeRoot(oldUrl, rootType);
        }
        return modified;
    }

    private ContentEntry getContentEntry(VirtualFile myContentRoot) {

        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            if (myContentRoot.equals(contentEntry.getFile())) {
                return contentEntry;
            }
        }
        return m_rootModel.addContentEntry(myContentRoot);
    }

    private void addSource(final Collection<SourceFolder> unusedSourceFolders, final Collection<ExcludeFolder> unusedExcludes, final Set<ContentEntry> unusedEntries, final ContentRoot[] contentRoots, ProgressIndicator progressIndicator) {
        //find a virtual file
        ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareRunnable() {
            public void run(ProgressIndicator progressIndicator) {

                for (ContentRoot contentRoot : contentRoots) {
                    VirtualFile virtualRootDir = LocalFileSystem.getInstance().findFileByIoFile(contentRoot.getDir());
                    if (virtualRootDir == null) {
                        UpdateModule.LOGGER.error("Unexisting root: " + virtualRootDir);
                    } else {
                        ContentEntry contentEntry = getContentEntry(virtualRootDir);
                        unusedEntries.remove(contentEntry);
                        File[] mainJavaDirs = contentRoot.getMainJavaDirs();
                        for (File mainJavaDir : mainJavaDirs) {
                            syncSource(contentEntry, mainJavaDir, unusedSourceFolders, false);
                        }
                        File[] testJavaDirs = contentRoot.getTestJavaDirs();
                        for (File testJavaDir : testJavaDirs) {
                            syncSource(contentEntry, testJavaDir, unusedSourceFolders, true);
                        }
                        File[] excludedDirs = contentRoot.getExcludedDirs();
                        for (File excludedDir : excludedDirs) {
                            VirtualFile fileByIoFile = LocalFileSystem.getInstance().findFileByIoFile(excludedDir);
                            if (fileByIoFile != null) {
                                Iterator<ExcludeFolder> unusedIterator = unusedExcludes.iterator();
                                while (unusedIterator.hasNext()) {
                                    ExcludeFolder excludeFolder = unusedIterator.next();
                                    if (fileByIoFile.equals(excludeFolder.getFile())) {
                                        unusedIterator.remove();
                                    }
                                }
                                contentEntry.addExcludeFolder(fileByIoFile);
                            }
                        }
                    }
                }
            }
        }, progressIndicator);
    }

    private void syncSource(ContentEntry contentEntry, File mainJavaDir, Collection<SourceFolder> unusedSourceFolders, boolean test) {
        VirtualFile fileByIoFile = LocalFileSystem.getInstance().findFileByIoFile(mainJavaDir);
        if (fileByIoFile != null) {
            Iterator<SourceFolder> iterator = unusedSourceFolders.iterator();
            while (iterator.hasNext()) {
                SourceFolder sourceFolder = iterator.next();
                if (fileByIoFile.equals(sourceFolder.getFile())) {
                    if (!(sourceFolder.isTestSource() ^ test)) {
                        iterator.remove();
                        return;
                    }
                }

            }
            contentEntry.addSourceFolder(fileByIoFile, test);
        }
    }

    public void run(final ProgressIndicator progressIndicator) {
        try {
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId());
            progressIndicator.setText2("");
            progressIndicator.setText("Synchronizing module " + m_resolvedModule.getModuleId());
            final List<String> dependantModules = new LinkedList<String>();
            final Map<ModuleId, File> modules = m_moduleManager.getModules();
            ModuleSourceDescriptor rootModuleDescriptor = m_moduleManager.getModuleSource(modules.get(m_resolvedModule.getModuleId()));
            File compilerOutputPath = rootModuleDescriptor.getCompilerOutputPath();
            if (compilerOutputPath != null) {
                String path = compilerOutputPath.getPath();
                String url = VirtualFileManager.constructUrl("file", path.replace(File.separatorChar, '/'));
                m_rootModel.setCompilerOutputPath(url);
            }
            File compilerOutputPathForTests = rootModuleDescriptor.getCompilerOutputPathForTests();
            if (compilerOutputPathForTests != null) {
                m_rootModel.setCompilerOutputPathForTests(VirtualFileManager.constructUrl("file", compilerOutputPathForTests.getPath().replace(File.separatorChar, '/')));
            }
            m_rootModel.setExcludeOutput(true);

            final List<Library> librariesUseByThisModule = new LinkedList<Library>();

            Map<ModuleId, DependencyType> internalModules = m_moduleManager.getDependantModules(m_resolvedModule);
            final Set<SourceFolder> unusedSourceFolders = new HashSet<SourceFolder>();
            final Set<ContentEntry> unusedContentEntries = new HashSet<ContentEntry>();
            final ContentEntry[] contentEntries = m_rootModel.getContentEntries();
            final Collection<ExcludeFolder> unusedExcludes = new LinkedList<ExcludeFolder>();
            ProgressUtilities.executeAsReadAction(new ProgressAwareCallable<Object>() {
                public Object call(ProgressIndicator indicator) throws Exception {
                    for (ContentEntry contentEntry : contentEntries) {
                        unusedContentEntries.add(contentEntry);
                        SourceFolder[] sourceFolders = contentEntry.getSourceFolders();
                        unusedSourceFolders.addAll(Arrays.asList(sourceFolders));
                        //getexcluded folders requires read access !!
                        unusedExcludes.addAll(Arrays.asList(contentEntry.getExcludeFolders()));
                    }
                    return null;
                }
            }, progressIndicator);
            addModuleSource(librariesUseByThisModule, unusedSourceFolders, unusedExcludes, unusedContentEntries, rootModuleDescriptor, progressIndicator, m_libraryModelsToCommit);

            Collection<ModuleRevisionId> dependencies = m_resolvedModule.getDependencies();
            for (final ModuleRevisionId moduleRevisionId : dependencies) {
                DependencyType dependencyType = internalModules.get(moduleRevisionId.getModuleId());
                UpdateModule.LOGGER.debug("DependencyTYpe for " + moduleRevisionId + " " + dependencyType);
                if (dependencyType == null || dependencyType.equals(DependencyType.LIBRARY)) {
                    Library library = synchronizeLib(m_usedProjectLibraries, m_projectLibraryTable, moduleRevisionId, m_moduleManager, m_resolvedModule, progressIndicator, m_libraryModelsToCommit);
                    librariesUseByThisModule.add(library);
                } else {
                    String dependantModuleName = moduleRevisionId.getModuleId().getName();
                    m_rootModel.addInvalidModuleEntry(dependantModuleName);
                    //this only works when committing models at the end...
//                    Module dependantModule = m_modulesModel.findModuleByName(dependantModuleName);
//                    LOGGER.debug("Adding module dependency to module " + m_rootModel.getModule().getName() + ": " + dependantModule);
//                    m_rootModel.addModuleOrderEntry(dependantModule);
                    dependantModules.add(dependantModuleName);
                    //ModuleSourceDescriptor moduleSourceDescriptor = m_moduleManager.getModuleSource(file);
                    //addModuleSource(librariesUseByThisModule, unusedSourceFolders, unusedContentEntries, moduleSourceDescriptor);
                }
            }
            /*
            FacetManager facetManager = FacetManager.getInstance(m_rootModel.getModule());


            ModifiableFacetModel facetModel = facetManager.createModifiableModel();
            EjbFacetConfiguration ejbFacetConfiguration = EjbFacetType.INSTANCE.createDefaultConfiguration();
            ConfigFileMetaData metaData =new ConfigFileMetaData(); 
            ejbFacetConfiguration.getDescriptorsConfiguration().getConfigFileInfo()
            FacetType<EjbFacet, FacetConfiguration> facetType = FacetTypeRegistry.getInstance().findFacetType(EjbFacet.ID);
            FacetConfiguration configuration = facetType.createDefaultConfiguration();

            EjbFacet ejbFacet = facetType.createFacet(m_rootModel.getModule(), "IvyEJB", configuration, null);
            ejbFacet.getEjbJarDescriptor().;

            facetModel.addFacet(ejbFacet);
            facetModel.commit();
            */
            progressIndicator.setText2("Cleaning unused libraries");
            ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareRunnable() {
                public void run(ProgressIndicator progressIndicator) {
                    synchronizeUsedLibraries(librariesUseByThisModule, m_rootModel.getModuleLibraryTable(), m_rootModel, true);
                    //This will sync the project libraries for this module, while previous statement syncs module libraries for the module..
                    synchronizeUsedLibraries(librariesUseByThisModule, m_projectLibraryTable, m_rootModel, false);
                    List<String> libraryNamesUsedByThisModule = new LinkedList<String>();
                    for (Library library : librariesUseByThisModule) {
                        libraryNamesUsedByThisModule.add(library.getName());
                    }
                    LOGGER.debug("Used libraries: " + libraryNamesUsedByThisModule);
                    cleanUnusedLibraries(librariesUseByThisModule, m_rootModel.getModuleLibraryTable());
                    cleanUnusedFolders(unusedSourceFolders, unusedExcludes, unusedContentEntries);
                    //remove unused and duplicate module dependencies
                    Set<String> orderEntries = new LinkedHashSet<String>();

                    //OEPS: Don't use enhanced for loop. It does not work when modifying the array (removeOrderEntry)
                    Collection<OrderEntry> entries = new LinkedList<OrderEntry>(Arrays.asList(m_rootModel.getOrderEntries()));
                    for (OrderEntry orderEntry : entries) {
                        if (orderEntry instanceof ModuleOrderEntry) {
                            String dependantModuleName = ((ModuleOrderEntry) orderEntry).getModuleName();
                            if (!dependantModules.contains(dependantModuleName)) {
                                LOGGER.debug("Removing unused module order entry: " + orderEntry.getPresentableName());
                                m_rootModel.removeOrderEntry(orderEntry);
                            } else {
                                checkDuplicates(orderEntries, orderEntry);
                            }
                        } else if (orderEntry instanceof LibraryOrderEntry) {
                            //don't remove unused here. It's already done before, and it's not correct on 7
                            //for new libraries, the libraryOrderEntry.getLibrary can be NULL. Don't know why..
                            LibraryOrderEntry libraryOrderEntry = (LibraryOrderEntry) orderEntry;
                            if (!libraryNamesUsedByThisModule.contains(libraryOrderEntry.getLibraryName())) {
                                LOGGER.debug("Removing unused library from order entries: " + libraryOrderEntry.getPresentableName() + " ( valid = " + libraryOrderEntry.isValid() + ")");
                                m_rootModel.removeOrderEntry(orderEntry);
                            } else {
                                checkDuplicates(orderEntries, orderEntry);
                            }
                        }
                    }
                }
            }, progressIndicator);
            progressIndicator.setText2("");
            LOGGER.debug("Updating " + m_resolvedModule.getModuleId() + " done");
        } catch (RuntimeException e) {
            throw e;
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    private VirtualFile[] createVirtualFiles(File[] files, ProgressIndicator progressIndicator) {
        Collection<VirtualFile> libDirs = new LinkedList<VirtualFile>();
        for (final File file : files) {
            final VirtualFile virtualFile;
            if (file.isDirectory()) {
                virtualFile = LocalFileSystem.getInstance().findFileByIoFile(file);
            } else {//for jar files, special care is needed..
                //both jar and zip files should be handled like this..
                final String urlString = VirtualFileManager.constructUrl("jar", (new StringBuilder()).append(file.getAbsolutePath().replaceAll("\\\\", "/")).append("!/").toString());
                virtualFile = VirtualFileManager.getInstance().findFileByUrl(urlString);
                //this sometimes leads to deadlocks..
                //at com.intellij.openapi.vfs.newvfs.persistent.PersistentFS.findRoot(PersistentFS.java:144)
//                virtualFile = ProgressUtilities.executeAsWriteActionOnEventQueue(new ProgressAwareCallable<VirtualFile>() {
//                    public VirtualFile call(ProgressIndicator progressIndicator) throws Exception {
//
//                        VirtualFile vf = VirtualFileManager.getInstance().findFileByUrl(urlString);
//                        VirtualFile vf = VirtualFileManager.getInstance().refreshAndFindFileByUrl(urlString);
//                        return vf;
//                    }
//                }, progressIndicator);
            }
            if (virtualFile != null) {
                libDirs.add(virtualFile);
            } else {
                LOGGER.debug("Unable to find virtual file for " + file);
            }
        }
        return libDirs.toArray(new VirtualFile[libDirs.size()]);
    }

    private void checkDuplicates(Set<String> orderEntries, OrderEntry orderEntry) {
        if (orderEntries.contains(orderEntry.getPresentableName())) {
            //duplicate
            LOGGER.debug("Removing duplicate order entry: " + orderEntry.getPresentableName());
            m_rootModel.removeOrderEntry(orderEntry);
        }
        orderEntries.add(orderEntry.getPresentableName());
    }

    private void cleanUnusedFolders(Collection<SourceFolder> unusedSourceFolders, Collection<ExcludeFolder> unusedExcludes, Set<ContentEntry> unusedEntries) {
        ContentEntry[] contentEntries = m_rootModel.getContentEntries();
        for (ContentEntry contentEntry : contentEntries) {
            for (SourceFolder sourceFolder : unusedSourceFolders) {
                if (contentEntry.equals(sourceFolder.getContentEntry())) {
                    contentEntry.removeSourceFolder(sourceFolder);
                }
            }
            if (unusedEntries.contains(contentEntry)) {
                m_rootModel.removeContentEntry(contentEntry);
            }

            for (ExcludeFolder unusedExclude : unusedExcludes) {
                if (!unusedExclude.isSynthetic()) {
                    if (contentEntry.equals(unusedExclude.getContentEntry())) {
                        LOGGER.debug("Removing unused exclude " + unusedExclude.getUrl() + " " + unusedExclude.isSynthetic());
                        contentEntry.removeExcludeFolder(unusedExclude);
                    }
                }
            }
        }
    }

    private void synchronizeUsedLibraries(final List<Library> usedLibraries, final LibraryTable projectLibraryTable, final ModifiableRootModel rootModel, final boolean export) {
        //for all entries in the model that are not used, remove order entry..
        Collection<Library> libraryIterator = new LinkedList<Library>(Arrays.asList(projectLibraryTable.getLibraries()));
        for (Library library : libraryIterator) {
            if (!usedLibraries.contains(library)) {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry != null) {
                    rootModel.removeOrderEntry(libraryOrderEntry);
                }
            } else {
                LibraryOrderEntry libraryOrderEntry = rootModel.findLibraryOrderEntry(library);
                if (libraryOrderEntry == null) {
                    //Altough the library is valid, we add an invalid library here...
                    //that works better on intellij 7. If we add the library directly,
                    //the returned order entry always gives back 'invalid' (for new libraries at least),
                    //and intellij get's messed up internally (red lines where there should not be..)
                    libraryOrderEntry = rootModel.addInvalidLibrary(library.getName(), projectLibraryTable.getTableLevel());
                }
                libraryOrderEntry.setExported(export);
            }
        }
    }

    static void cleanUnusedLibraries(Collection<Library> usedLibraries, LibraryTable libraryTable) {
        Collection<Library> libraries = new LinkedList<Library>(Arrays.asList(libraryTable.getLibraries()));
        List<String> usedLibraryNames = new LinkedList<String>();
        for (Library library : libraries) {
            if (!usedLibraries.contains(library)) {
                //remove
                LOGGER.debug("Removing unused library: " + library.getName() + " " + library);
                libraryTable.removeLibrary(library);
            } else if (usedLibraryNames.contains(library.getName())) {
                //removing duplicate..
                LOGGER.debug("Removing duplicate library: " + library.getName() + " " + library);
                libraryTable.removeLibrary(library);
            }
            usedLibraryNames.add(library.getName());
        }
    }

    private void addModuleSource(List<Library> used, Collection<SourceFolder> unusedSourceFolders, Collection<ExcludeFolder> unusedExcludes, Set<ContentEntry> unusedEntries, ModuleSourceDescriptor rootModuleDescriptor, ProgressIndicator progressIndicator, Set<Library.ModifiableModel> libModifiableModels) {
        ContentRoot[] contentRoots = rootModuleDescriptor.getContentRoots();
        addSource(unusedSourceFolders, unusedExcludes, unusedEntries, contentRoots, progressIndicator);

        LibraryTable libraryTable = m_rootModel.getModuleLibraryTable();

        for (ContentRoot contentRoot : contentRoots) {
            final List<File> libDirs = new LinkedList<File>();
            libDirs.addAll(Arrays.asList(contentRoot.getMainResourceDirs()));
            libDirs.addAll(Arrays.asList(contentRoot.getTestResourceDirs()));
            if (libDirs.size() > 0) {
                used.add(synchronizeLib(libraryTable, contentRoot.getDir().getPath(), new LibraryDescriptorImpl(libDirs.toArray(new File[libDirs.size()])), progressIndicator, libModifiableModels));
            }
        }
    }

}
